Repository navigation
feat(evaluations)!: route handlers by provider and mode - #152
Conversation
Implement the handler routing rules of TESTING.md §8 in launchdarkly/ai-sdks-monorepo#52. - run() takes `handler` or `handlers`, not both. `judge_handlers` is removed. Every handler must declare provides_for, and two handlers cannot declare the same provider and mode. - Generation and every judge select a handler with the config() rule (utils.select_handler). There is no mode fallback, so a messages-mode judge never runs on an agent handler. - generation.mode is required. The values are "completion" and "agent". GenerationConfig stays a TypedDict with Required fields. GenerationOverrides is the type for overrides with ai_config. - The prompt must match the handler mode. A messages handler needs messages and an agent handler needs instructions. The harness does not convert one into the other. - Judges get no tools. - ai_config reads the AI Config mode first, then the variation, then the model config. Each check runs right after the read that supplies its input. The mode selects the prompt field of the variation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeffdupont
left a comment
There was a problem hiding this comment.
Reviewed at 26eafe03, against spec §8.4 as merged in monorepo #52. The routing rules match: one select_handler rule for generation and judges, no mode fallback, generation.mode checked before normalize_mode (module.py:852-856, with 'judge' and 'agnet' tested), prompt/mode checks, and judges with no tools. make test: 1565 passed, 11 skipped. This also closes the judge_handlers divergence that monorepo #56 had deleted the note for.
- A handler tagged
"completion", or with a list, is accepted and then never selected (inline atrunner.py:117; same finding as the open Devin and Bugbot threads)._validate_handlersnormalizes the tag, so("OpenAI", "completion")counts as("OpenAI", "messages").test_evaluations_run.py:4300even relies on this to reject a duplicate.- But
utils.select_handlercompares the raw tag:h.provides_for == (provider, mode)(utils.py:516), and[1] == modefor wildcards (:524). - I ran it.
create_handler(("OpenAI", "completion"), fn)and["OpenAI", "messages"]both pass validation, butselect_eval_handlerreturnsNone. The error then lists the handler as('OpenAI', 'messages'), the very pair it needed. - This PR tells users to write
mode: "completion", so that tag is the natural one to reach for. Onlineconfig()misses it the same way, because it sharesselect_handler. - Fix it once in
select_handler(utils.py, not in this diff): compare against(h.provides_for[0], normalize_mode(h.provides_for[1])), and use the same normalized value in the wildcard branch. That's about 3 lines, and it fixesconfig()too. A test with a lone("OpenAI", "completion")handler would cover it.
- The two new types need to reach #157's public path (inline at
evaluations/__init__.py:36). I trial-merged this onto #157. The only conflict ispackages/ai/README.md: take #157's side, since it removes that example. The merged suite passes (2418 passed). ButGenerationModeandGenerationOverridesend up only inlaunchdarkly_ai_server.evaluations, which #157 makes internal. They aren't inlaunchdarkly_ai_server.experimental.evaluations. Whichever of the two lands second should add both names toexperimental/evaluations.py__all__and to #157's public-surface test. I'll add+ GenerationModeand+ GenerationOverridesto the API list in monorepo #51.
Not checked: the ai_config GET order (I went by the tests, I didn't probe it).
…routing # Conflicts: # packages/ai/README.md
select_handler compared a handler's raw provides_for, so a handler tagged
("OpenAI", "completion") or with a list passed validation and was never
selected. It now compares the provider and the normalized mode. This also
fixes config(), which uses the same function.
Export GenerationMode and GenerationOverrides from
launchdarkly_ai_server.experimental.evaluations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lift select_handler's mode-normalized tag into utils.handler_key so Registry.register, compose, select_handler, and evaluations duplicate checks use one identity rule. completion and messages collide; last registration wins in the registry.
run_judges / build_judge_tasks / run_judge compared raw provides_for modes, so a completion registration that replaced messages missed messages-mode judges and fell through to agent (or returned None). Co-authored-by: doneill <doneill@launchdarkly.com>
…to claude/evals-handler-routing # Conflicts: # packages/client/src/launchdarkly_ai_server/judges.py
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit be009fe. Configure here.
| prompt_error = prompt_mode_error(judge_config, resolved_judge.mode) | ||
| if prompt_error is not None: | ||
| problems.append(f"judge {judge.key!r}: {prompt_error}") | ||
| continue |
There was a problem hiding this comment.
Judge prompt check rejects dual fields
High Severity
_resolve_judges runs prompt_mode_error on the raw flag-delivery judge config. Generation first keeps only the mode's prompt field via from_api, but judges skip that step, so a config that has both instructions and messages fails the check even when the mode-appropriate field is present and the run never starts.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit be009fe. Configure here.


Summary
This change implements the handler routing rules of the shared spec,
TESTING.md§8, in launchdarkly/ai-sdks-monorepo#52. It lets one evaluation run use different providers or modes for generation and for judges. For example, generation uses OpenAI and a judge uses Anthropic, or generation uses an agent handler and a judge uses a messages handler.This is a breaking change to the experimental
evaluationsAPI.handlerorhandlersrun()takes one handler ashandleror a list ashandlers, not both.judge_handlersis removed, with no alias.provides_for. Before this change, a handler without it received every judge config with no warning.One selection rule
utils.select_handler, the same rule asconfig(). The evals code has no copy of it.collapse_messagesis removed from the evals path. The onlinejudges.pypath does not change.generation.modemodeis required:"completion"or"agent". There is no default."messages","judge"or a typo, raise before any network I/O. They do not go throughnormalize_mode.GenerationConfigstays aTypedDictwithRequired[...], so callers pass a plaindictand import no type. The newGenerationOverridesTypedDictis the type for overrides withai_config. Two@overloadsignatures onrun()select between them.GenerationConfigdocstring and the README tell the user how to choose a mode.Prompt must match the handler mode
instructionsraises: "the generation prompt needsmessages".messagesraises: "the generation prompt needsinstructions".Judges get no tools
A judge call gets an empty tool map, and
toolsis removed from the judge config.ai_configGET ai-configs/<key>to get the mode. The mode is on the AI Config, not on the variation.instructionsin agent mode,messagesin completion and judge mode.mode.Migration
To evaluate your own code, wrap it:
create_handler(("OpenAI", "messages"), my_app).Tests
mode. The judge fixtures now usemessages, which is what judge configs carry.ai_configfailuremake testafter the merge withmain: 2583 passed, 14 skipped.make lint,make format-checkandmake typecheckare clean.Review fixes
utils.select_handlernow compares a handler's provider and its mode afternormalize_mode. Before this change, a handler tagged("OpenAI", "completion")or with a list passed validation but was never selected. The fix is in the shared function, so the onlineconfig()path is fixed too. Tests for both paths fail without the fix.GenerationModeandGenerationOverridesare exported fromlaunchdarkly_ai_server.experimental.evaluations, which is the public path since feat(evaluations)!: mark evaluations from code as experimental #157. They are also in its surface test.main. Thepackages/ai/README.mdconflict takes the side ofmain.Out of scope
toolsmap shape and itsGET ai-toolsstep at run time. Python useslist[EvalTool]andtools.get(), which was an earlier divergence.BEGIN_COMMIT_OVERRIDE
feat(evaluations)!: route handlers by provider and mode
fix(client): match a handler's provides_for after normalizing its mode
END_COMMIT_OVERRIDE
🤖 Generated with Claude Code
Note
Overview
Breaking change to the experimental offline evaluations API: handler routing now follows the same provider/mode rules as online
config(), with stricter validation up front.run()accepts eitherhandlerorhandlers(not both);judge_handlersis removed. Every handler must declareprovides_for, and duplicates for the same provider+mode are rejected. Generation and each judge pick a handler via sharedselect_handler/handler_key(withcompletionandmessagestreated as one mode). There is no mode fallback in evals—a messages judge never runs on an agent handler, and judge message collapsing is removed on this path.generation.modeis required ("completion"or"agent", no default). Prompt shape must match the selected handler (messagesvsinstructions), for generation and judges; judge setup failures are aggregated into one error before any run records are created. Judges run with no tools (empty tool map,toolsstripped from judge config).ai_configruns read AI Config mode first, then variation, then linked model config, validating handler coverage, prompt, and provider after each step. Overrides use newGenerationOverrides(nomodeoverride). TypesGenerationModeandGenerationOverridesare exported from the experimental evaluations module. Docs and tests are updated throughout; onlinejudges.pygains normalizedprovides_formatching but keeps agent fallback for messages judges.Reviewed by Cursor Bugbot for commit be009fe. Bugbot is set up for automated code reviews on this repo. Configure here.